Repository Public
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Fiche complète d'une propriété à vendre (copie Immo-Ka) + mesure Vrai-Prix.3import type { Metadata } from "next";4import { cache } from "react";5import { notFound } from "next/navigation";6import { getUnit } from "@/lib/db";7import { getListing, listingsNear } from "@/lib/immoka";8import ListingView, { type ListingPageData } from "@/components/avendre/ListingView";9import { moneyFr } from "@/lib/seo";10import { summaryForListing } from "@/lib/cost/ai/service";1112export const dynamic = "force-dynamic";1314const load = cache((uid: string): ListingPageData | null => {15 const det = getListing(uid);16 if (!det) return null;17 const l = det.listing;18 const u = det.eval?.unit_id ? getUnit(det.eval.unit_id) : undefined;19 const nearby =20 l.lat != null && l.lng != null21 ? listingsNear(l.lat, l.lng, 3, { group: det.group, excludeUid: uid, limit: 6 })22 : [];23 return {24 listing: {25 uid: l.uid,26 source: l.source,27 url: l.url,28 title: l.title,29 address: l.address,30 sector: l.sector,31 city: l.city,32 region: l.region,33 propertyType: l.property_type,34 price: l.price,35 bedrooms: l.bedrooms,36 bathrooms: l.bathrooms,37 powderRooms: l.powder_rooms,38 areaSqft: l.area_sqft,39 lotSqft: l.lot_sqft,40 yearBuilt: l.year_built,41 mls: l.mls,42 brokerName: l.broker_name,43 agency: l.agency,44 description: l.description,45 lat: l.lat,46 lng: l.lng,47 firstSeen: l.first_seen,48 lastSeen: l.last_seen,49 },50 group: det.group,51 images: det.images,52 features: det.features,53 details: det.details,54 priceLog: det.priceLog,55 eval: det.eval,56 unit: u57 ? {58 id: u.id_provinc,59 adresse: u.adresse,60 apt: u.apt,61 municipalite: u.municipalite,62 typeProp: u.type_prop,63 anneeConstruction: u.annee_construction,64 aireEtagesM2: u.aire_etages_m2,65 superficieTerrainM2: u.superficie_terrain_m2,66 nbEtages: u.nb_etages,67 nbLogements: u.nb_logements,68 genreConstruction: u.genre_construction,69 lienPhysique: u.lien_physique,70 valeurTerrain: u.valeur_terrain,71 valeurBatiment: u.valeur_batiment,72 valeurRole: u.valeur_role,73 est2026: u.est_2026,74 p10: u.p10,75 p90: u.p90,76 history: ([2021, 2022, 2023, 2024, 2025, 2026] as const).map((y) => ({77 year: y,78 value: u[`est_${y}`] as number | null,79 })),80 }81 : null,82 nearby,83 aiSummary: safeSummary(uid),84 };85});8687/** Résumé de l'analyse IA du bâtiment (chantier Coût) — jamais bloquant pour la fiche. */88function safeSummary(uid: string) {89 try { return summaryForListing(uid); } catch { return null; }90}9192export async function generateMetadata({ params }: { params: Promise<{ uid: string }> }): Promise<Metadata> {93 const { uid } = await params;94 const d = load(decodeURIComponent(uid));95 if (!d) return { title: "Propriété introuvable" };96 const l = d.listing;97 const where = [l.address, l.city].filter(Boolean).join(", ") || l.title || "Propriété à vendre";98 const est = d.eval?.est ? ` · estimation Vrai-Prix ${moneyFr(d.eval.est)}` : "";99 const title = `${where} — ${moneyFr(l.price)}${est}`;100 const description = `${l.propertyType ?? "Propriété"} à vendre à ${l.city ?? "—"} : prix demandé ${moneyFr(101 l.price102 )}${d.eval?.est ? `, estimation Vrai-Prix ${moneyFr(d.eval.est)} (${moneyFr(d.eval.low)} – ${moneyFr(d.eval.high)})` : ""}. Choisissez vos comparables et comparez votre évaluation à la mesure Vrai-Prix.`;103 return {104 title,105 description,106 alternates: { canonical: `/a-vendre/${encodeURIComponent(l.uid)}` },107 openGraph: { title, description, type: "website", images: d.images[0] ? [{ url: d.images[0] }] : undefined },108 robots: { index: false, follow: true }, // annonces éphémères, contenu tiers109 };110}111112export default async function ListingPage({ params }: { params: Promise<{ uid: string }> }) {113 const { uid } = await params;114 const data = load(decodeURIComponent(uid));115 if (!data) notFound();116 return <ListingView data={data} />;117}118